-
-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(observable): coercion, typed observable #623
Conversation
Very nice 💪- have you had a chance to try this out in data-binding scenarios? I imagine there must be some gotchas when you attempt to use this with two-way binding. What are the limitations that folks should know about? What are some good example use-cases we can put in the docs for this feature? |
@jdanyow For two way data binding on text field, it is normal with string, ok-ish with number, and broken for any other type (date, boolean). I feel like there's a lot of gotchas, but cannot really describe them. I've prepared a gist at https://gist.run/?id=7d59c430a382b3423a13d7f760eb2c00 to demo common cased for typed observable and bindable. ** There is a small modification for date coerce function date: function(val) {
// instead of return new Date(val);
if (val === null || val === undefined) {
return null;
}
const d = new Date(val);
const t = d.getTime(); // to deal with invalid date
return t === t ? d : null;
} Edit 1: Another concerns is Edit 2: I can start putting together a list of gotchas here Gotchas
@jdanyow For gotacha 1, maybe we can employ a What do you think ? |
@jdanyow @EisenbergEffect Explanation: When doing: javascript using class MyClass {
@Reflect.metadata('design:Type', Number) age
} or typescript: emit metadata: class MyClass {
age: number
} We are doing basically this: Reflect.defineMetadata('design:Type', Number, MyClass.prototype, 'age'); The work around is probably the decorators have too loop over the prototype chain and find the corresponding metadata for the properties. What do you think @jdanyow @EisenbergEffect |
Wow @bigopon . 💪💪💪💪💪 |
@jdanyow @EisenbergEffect sorry false alarm. Information about coercion for inheritance in both One thing should be discussed is how we want the API for turn on / off the flag to auto pickup type metadata. Two options atm: let _usePropertyType = false;
export function usePropertyType(shouldUsePropertyType: boolean) {
_usePropertyType = shouldUsePropertyType;
}
// OR
observable.usePropertyType = function(shouldUsePropertyType: boolean) {
_usePropertyType = shouldUsePropertyType;
} Working on |
src/coerce-functions.js
Outdated
} | ||
const d = new Date(val); | ||
const t = d.getTime(); // to deal with invalid date | ||
return t === t ? d : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this t === t
an optimization compared to !isNaN(t)
, or why do you use this weird syntax, that only makes sense in JavaScript?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it is used to specifically deal with NaN value. As NaN === NaN
is false
.
What a spec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But does it perform better than using isNaN
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats interesting question. Normally i would say yes but V8 is smart and I believe isNaN could be faster ( from another v8 engineer tweet i saw different between o === o
and Object.is(o, o)
is 2 us
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As
NaN === NaN
isfalse
.
Yeah i know, right after figuring that out I started using TsLint ;)
My comment+question was because of readability & preformance. IMHO if t === t
is not preforming (much) better than !isNaN(t)
, then i'd vote for better readability :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Cant commit on phone 😁 Will change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@atsu85 thanks for the suggestion. I just keep doing those things 😄
Demo for this PR + typed bindable is at http://aurelia-typed-observable-bindable.surge.sh/ IE 11 compatible so we can test the binding scenarios for all supported browsers. |
@EisenbergEffect This PR is base for aurelia/templating#558, and is waiting for some review from @jdanyow . Beside one thing that is I can no longer break it when setting coerce (and create new object to assign) on both side of two way binding, which is quite unexpected. If this is good to merge then the other can be merged straight away I believe |
@bigopon I didn't quite follow that last bit. Are you saying that two-way patterns work now as well? |
@bigopon, @EisenbergEffect it would be great to have a few reference use-cases for coerce so we can validate our implementation works for them. I'm worried coerce will cause a bunch of confusion due to the view and view-model values not matching and the odd scenarios that will result, similar to what happens when folks attempt to use a value converter or a property setter to make a number/date control. If you look through the binding issues folks have opened there are a bunch related to folks being surprised that the html input element's value property coerces to string. Here's a few: |
@EisenbergEffect Just rechecked it, it is working as expected, In the gist I wasn't declaring |
}, | ||
number(a) { | ||
const val = Number(a); | ||
return !isNaN(val) && isFinite(val) ? val : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect either NaN or Infinity to be returned if not a valid number, not 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NaN
doesn't work nicely with ===
(two way binding scenarios), to handle it we need to sacrifice perf for more checks. With hundreds of bindings perf hit will be worse. I'm not sure what to do here. Infinity
, on the other hand, still problematic but could be treated as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it is a problem with two-way binding. Still, as a developer I would not expect 0 to be returned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be application specific where NaN
/ Infinity
are expected. For that case, we can always define another decorator via createTypedObservable
:
import {
coerceFunctions,
createTypedObservable
} from 'aurelia-binding'
coerceFunctions.$number = function(value) {
return Number(value);
};
createTypedObservable('$number');
// usage
class App {
@observable.$number() numProp
}
boolean(a) { | ||
return !!a; | ||
}, | ||
date(val) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since dates are stateful, should there be a check if it's already of type date?
I haven't thought this through, just want to make sure this case has been considered.
if (typeof val === 'date') {
return val;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be nice. I didn't think of it. Maybe we can reuse the instance if this ever gets merged ? @jdanyow / @EisenbergEffect
Aurelia does not provide any value converters so why would the coerce functions be provided? |
@gheoan Imo, it is good to have interface used for dependencies declaration. In JS, there is no interface so string is the best option. Plus there are unexpected behavior when you call primitive constructor directly, for example Date(new Date()) !== new Date(new Date()) And how do you override existing / built in implementation if everything is a function ? It is sometime desired to do so. For example, in this PR implementation, we have identical behavior between: class ABC {
@observable({ coerce: 'string' }) value
} class ABC {
@observable({ coerce: String }) value
} because they both dont care import { RealStringConverter } from './util'
class ABC {
@observable({ coerce: RealStringConverter }) value
} While with string: // main.js
import { coerceFunctions } from 'aurelia-binding'
coerceFunctions.string = val => val === null || val === undefined ? '' : val.toString();
// app.js
class ABC {
@observable({ coerce: 'string' }) value
} Does the above answer your question ? I do want to see the PR get merged, but like @jdanyow stated, it would be nice if we could have real world usage to validate the implementation. It works fine in tests but the experience could vary in app. |
@EisenbergEffect @jdanyow Maybe this could be enabled via plugin form, so people can try it out and before we include it in the box ? |
agree- plugin would be better for something like this. I'm worried that it will be hard to support given all the issues mentioned in #623 (comment) |
For anyone interested in this feature: https://github.com/bigopon/aurelia-typed-observable-plugin |
Thanks @bigopon for the plugin. My only question is that if we use this, is there much concern over future aurelia binding releases? Are you all rewriting this or just extending the binding from aurelia? |
This PR
observable
observable
with fluent syntax:@observable.custom
@observable.date
,@observable.number
,@observable.string
,@observable.boolean
Usage:
With normal syntax
Using metadata
TypeScript users can achieve above result (metadata) by simpler code:
Instruction for the compiler to emit decorator metadata TypeScript decorator doc
All coerce type will be resolved to a string, which then is used to get the converter function in
coerceFunctions
export of this module. So, to extend or modify basic implementations:For TS users or JS users who want to use metadata, to extend coerce mapping:
With fluent syntax
To built your own fluent syntax observable: